﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Specialized;
using System.IO;
using System.Runtime.InteropServices;
using System.Net.Mail;
using System.Configuration;
using System.Diagnostics;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Auth;
using Microsoft.WindowsAzure.Storage.Blob;

namespace AzureBackup
{
    class Program
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);
        [FlagsAttribute]

        public enum EXECUTION_STATE : uint
        {
            ES_SYSTEM_REQUIRED = 0x00000001,
            ES_DISPLAY_REQUIRED = 0x00000002,
            // Legacy flag, should not be used.
            // ES_USER_PRESENT   = 0x00000004,
            ES_CONTINUOUS = 0x80000000,
        }

        [DllImport("Kernel32")]
        private static extern Boolean GetSystemPowerStatus(SystemPowerStatus sps);
        // Fields must mirror their unmanaged counterparts, in order
        [StructLayout(LayoutKind.Sequential)]
        public class SystemPowerStatus
        {
            public ACLineStatus _ACLineStatus;
            public BatteryFlag _BatteryFlag;
            public Byte _BatteryLifePercent;
            public Byte _Reserved1;
            public Int32 _BatteryLifeTime;
            public Int32 _BatteryFullLifeTime;
        }
        public enum ACLineStatus : byte
        {
            Offline = 0, Online = 1, Unknown = 255
        }
        public enum BatteryFlag : byte
        {
            High = 1,
            Low = 2,
            Critical = 4,
            Charging = 8,
            NoSystemBattery = 128,
            Unknown = 255
        }

        static int noErrors = 0;
        static int noWarnings = 0;
        static string logPath;
        static bool incrementalBackup = false;
        static bool doCleanAtEnd = false;

        static void Main(string[] args)
        {
            try
            {
                logPath = Path.GetTempPath() + "AzureBackupLog_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".txt";
                if (args.Length >= 1)
                {
                    try
                    {
                        string command = args[0];
                        //string backupName = ;
                        if (command.Equals("backup", StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length >= 3)
                            {
                                WriteInfoLine("Log path: " + logPath);
                                //Console.WriteLine(args[3]); 
                                if (args.Length > 3)
                                {
                                    for (int i = 3; i < args.Length; i++)
                                    {
                                        string arg = args[i];
                                        //MIRROR OR INCREMENTAL
                                        if (arg.ToUpper() == "/MIR")
                                        {
                                            doCleanAtEnd = true;
                                            incrementalBackup = true;
                                            WriteInfoLine("Incremental backup using file archive bit, with clean");
                                        }
                                        else if (arg.ToUpper() == "/INC")
                                        {
                                            incrementalBackup = true;
                                            WriteInfoLine("Incremental backup using file archive bit");
                                        }
                                    }
                                }
                                var result = Backup(InitializeStorage(args[1].ToLower()), args[2]);
                                if (doCleanAtEnd)
                                {
                                    WriteInfoLine("Starting clean process");
                                    Clean(InitializeStorage(args[1].ToLower()), args[2]);
                                }
                                SendEmail(args[1].ToLower(), result);
                            }
                            else
                                WriteError("Usage: AzureBackup backup <backup name> <source path> </INC|/MIR>");
                        }
                        else if (command.Equals("list", StringComparison.OrdinalIgnoreCase))
                        {
                            List(InitializeStorage(args[1].ToLower()));
                        }
                        else if (command.Equals("clear", StringComparison.OrdinalIgnoreCase))
                        {
                            Clear(InitializeStorage(args[1].ToLower()));
                        }
                        else if (command.Equals("restore", StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 3)
                                Restore(InitializeStorage(args[1].ToLower()), args[2]);
                            else
                                WriteError("Usage: AzureBackup restore <backup name> <destination path>");
                        }
                        else if (command.Equals("clean", StringComparison.OrdinalIgnoreCase))
                        {
                            if (args.Length == 3)
                                Clean(InitializeStorage(args[1].ToLower()), args[2]);
                            else
                                WriteError("Usage: AzureBackup clean <backup name> <destination path>");
                        }
                        else
                        {
                            WriteError("Unknown command " + command);
                            WriteUsage();
                        }
                    }
                    catch (System.Net.WebException we)
                    {
                        WriteError("Network error: " + we.Message);
                        if (we.Status == System.Net.WebExceptionStatus.ConnectFailure)
                        {
                            WriteError("Please check if the blob storage service is running.");
                        }
                    }
                    catch (StorageException se)
                    {
                        WriteError("Storage service error: " + se.Message);
                    }
                }
                else
                    WriteUsage();
            }
            catch (Exception ex)
            {
                WriteError(ex.Message);
            }
            finally
            {
                //WriteInfoLine("Log path: " + logPath);
                //Console.Read(); //KEEP WINDOW OPEN DURING DEBUG
            }
        }

        private static void WriteUsage()
        {
            WriteError("Usage: AzureBackup <command> <backup name> <command specific arguments>");
            WriteError("Commands: backup, list, clear, clean, restore");
        }

        private static CloudBlobContainer InitializeStorage(string containerName)
        {
            CloudStorageAccount account = CloudStorageAccount.Parse(ConfigurationManager.ConnectionStrings["StorageConnectionString"].ConnectionString);
            WriteInfoLine(string.Format("Using account {0}, endpoint {1}, container {2}.", account.Credentials.AccountName, account.BlobEndpoint.ToString(), containerName));
            CloudBlobClient blobClient = account.CreateCloudBlobClient();
            // Retrieve a reference to a container. 
            CloudBlobContainer container = blobClient.GetContainerReference(containerName);
            // Create the container if it doesn't already exist.
            container.CreateIfNotExists();
            return container;
        }

        private static BackupResult Backup(CloudBlobContainer container, string sourcePath)
        {
            //Console.Read();
            SystemPowerStatus sps = new SystemPowerStatus();
            GetSystemPowerStatus(sps);
            if (sps._ACLineStatus != ACLineStatus.Online)
                WriteWarning("Warning: The computer is not on AC power.");
            SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS | EXECUTION_STATE.ES_SYSTEM_REQUIRED);
            WriteInfoLine("Backup started.");
            // Get all file names
            if (!sourcePath.EndsWith("\\"))
                sourcePath += "\\";
            string[] fileNames = Directory.GetFiles(sourcePath, "*", SearchOption.AllDirectories);
            //WriteInfoLine(fileNames.Length + " files.");
            long totalSize = 0;
            long backupSize = 0;
            long archiveFiles = 0;
            foreach (string fileName in fileNames)
            {
                //CHECK FOR INCREMENTAL OR MIRROR ATTRIBUTES, THEN ADD TO totalSize ONLY WHEN ARCHIVE ATTRIBUTE IS NOT SET.
                FileInfo fi = new FileInfo(fileName);
                bool archive = ((File.GetAttributes(fileName) & FileAttributes.Archive) == FileAttributes.Archive);
                //FileAttributes fa = fi.Attributes;
                if (archive)
                {
                    backupSize += fi.Length;
                    archiveFiles++;
                }
                totalSize += fi.Length;
            }
            WriteInfoLine(string.Format("{0} files in backup directory, totaling {1:N} MB.", fileNames.Length, totalSize / 1024 / 1024));
            if (incrementalBackup)
            {
                totalSize = backupSize;
                WriteInfoLine(string.Format("{0} files to back up, totaling {1:N} MB.", archiveFiles, backupSize / 1024 / 1024));
            }
            var result = new BackupResult(fileNames.Length);
            // Backup each file
            foreach (string fileName in fileNames)
            {
                BackupFile(sourcePath, fileName.Substring(sourcePath.Length), totalSize, container, result);
            }
            WriteInfoLine("Backup completed.");
            WriteInfoLine("Number of source files: " + result.Files);
            WriteInfoLine("Number of created files: " + result.Created);
            WriteInfoLine("Number of updated files: " + result.Updated);
            WriteInfoLine("Number of unchanged files: " + result.Unchanged);
            WriteInfoLine("Number of errors: " + noErrors);
            WriteInfoLine("Number of warnings: " + noWarnings);
            SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS);
            return result;
        }

        private static void BackupFile(string rootPath, string relativePath, long totalSize, CloudBlobContainer container, BackupResult result)
        {
            
            try
            {
                bool archive = false;
                if (incrementalBackup)
                {
                    //GET LOCAL FILE ATTRIBUTES AND CHECK ARCHIVE BIT
                    archive = ((File.GetAttributes(rootPath + relativePath) & FileAttributes.Archive) == FileAttributes.Archive);
                    if (!archive)
                    {
                        result.Unchanged++;
                        return;
                    }
                }
                //CONTINUE
                FileInfo fileInfo = new FileInfo(rootPath + relativePath);
                //WriteInfoLineEnd(".");
                WriteInfo(string.Format("{0} ({1:N0} KB): ", relativePath, fileInfo.Length / 1024));
                result.BackedUpSize += fileInfo.Length;
                CloudBlockBlob blockBlob = container.GetBlockBlobReference(relativePath);
                try
                {
                    blockBlob.FetchAttributes();
                    // Exists
                    BlobProperties props = blockBlob.Properties;
                    if (props.LastModified < fileInfo.LastWriteTime.ToUniversalTime() || (archive && incrementalBackup))
                    {
                        // Update
                        DateTime start = DateTime.Now;
                        using (FileStream fs = File.Open(rootPath + relativePath, FileMode.Open, FileAccess.Read))
                        {
                            blockBlob.UploadFromStream(source: fs, options: new BlobRequestOptions { ServerTimeout = CalcTimeout(fileInfo.Length, Direction.Up) });
                        }
                        //WriteInfoLineEnd(".");
                        WriteInfoLineEnd(string.Format("updated @ {1:N0}K/sec - {0:P}", (float)result.BackedUpSize / (float)totalSize, fileInfo.Length / 1024 / DateTime.Now.Subtract(start).TotalMilliseconds * 1000));
                        //SET ARCHIVE BIT
                        File.SetAttributes(rootPath + relativePath, File.GetAttributes(rootPath + relativePath) & ~FileAttributes.Archive);
                        result.Updated++;
                    }
                    else
                    {
                        WriteInfoLineEnd(string.Format("unchanged. {0:P}", (float)result.BackedUpSize / (float)totalSize));
                        //WriteInfo(".");
                        //SET ARCHIVE BIT EVEN IF UNCHANGED
                        File.SetAttributes(rootPath + relativePath, File.GetAttributes(rootPath + relativePath) & ~FileAttributes.Archive);
                        result.Unchanged++;
                    }
                }
                catch (StorageException ex)
                {
                    if (ex.RequestInformation.HttpStatusCode == 404)
                    {
                        // Blob does not exists - create
                        DateTime start = DateTime.Now;
                        using (FileStream fs = File.Open(rootPath + relativePath, FileMode.Open, FileAccess.Read))
                        {
                            blockBlob.UploadFromStream(source: fs, options: new BlobRequestOptions { ServerTimeout = CalcTimeout(fileInfo.Length, Direction.Up) });
                        }
                        WriteInfoLineEnd(string.Format("created @ {1:N0}K/sec - {0:P}", (float)result.BackedUpSize / (float)totalSize, fileInfo.Length / 1024 / DateTime.Now.Subtract(start).TotalMilliseconds * 1000));
                        //SET ARCHIVE BIT
                        File.SetAttributes(rootPath + relativePath, File.GetAttributes(rootPath + relativePath) & ~FileAttributes.Archive);
                        result.Created++;
                    }
                    else
                        WriteWarning(string.Format("Error while backing up file {0}: {1} - {2}", rootPath + relativePath, ex.Message, ex.InnerException));
                }
            }
            catch (Exception ex)
            {
                WriteWarning(string.Format("Error while backing up file {0}: {1} - {2}", rootPath + relativePath, ex.Message, ex.InnerException));

            }
        }

        private enum Direction { Up, Down };

        private static TimeSpan CalcTimeout(long size, Direction direction)
        {
            float speed;
            double seconds;
            if (direction == Direction.Up)
                speed = Properties.Settings.Default.UploadSpeed;
            else
                speed = Properties.Settings.Default.DownloadSpeed;
            // Calculate theoretical time and multiply by 5 to be on the safe side.
            seconds = Math.Round((size * 8) / (speed * 1024 * 1024) * 5);
            if (seconds <= 0)
                seconds = 1;
            return TimeSpan.FromSeconds(seconds);
        }

        private static void Restore(CloudBlobContainer container, string destination)
        {
            SystemPowerStatus sps = new SystemPowerStatus();
            GetSystemPowerStatus(sps);
            if (sps._ACLineStatus != ACLineStatus.Online)
                WriteWarning("Warning: The computer is not on AC power.");
            SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS | EXECUTION_STATE.ES_SYSTEM_REQUIRED);

            if (!destination.EndsWith("\\"))
                destination += "\\";
            int no = 0;
            // enumerate all the blobs
            foreach (IListBlobItem item in container.ListBlobs(prefix: null, useFlatBlobListing: true, blobListingDetails: BlobListingDetails.None))
            {
                if (item.GetType() == typeof(CloudBlockBlob))
                {
                    CloudBlockBlob blockBlob = (CloudBlockBlob)item;
                    WriteInfo(string.Format("{0} ({1} KB): ", blockBlob.Name, blockBlob.Properties.Length / 1024));
                    no++;
                    try
                    {
                        if (!Directory.Exists(Path.GetDirectoryName(destination + blockBlob.Name)))
                            Directory.CreateDirectory(Path.GetDirectoryName(destination + blockBlob.Name));
                        using (FileStream fs = File.Open(destination + blockBlob.Name, FileMode.Create))
                        {
                            blockBlob.DownloadToStream(fs);
                            fs.Close();
                        }
                        WriteInfoLineEnd("restored.");
                        no++;
                    }
                    catch (System.IO.IOException ex)
                    {
                        WriteWarning(ex.Message);
                    }
                }
                else
                {
                    throw new NotImplementedException(item.GetType().Name);
                }
            }

            WriteInfoLine(string.Format("{0} files restored.", no));
            SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS);
        }

        private static void Clear(CloudBlobContainer container)
        {
            Console.Write("This will delete the entire backup. Do you want to continue? Y/N ");
            string answer = Console.ReadLine().Trim();
            if (answer == "Y" || answer == "y")
                container.Delete();
        }

        private static void List(CloudBlobContainer container)
        {
            int no = 0;
            // enumerate all the blobs
            foreach (IListBlobItem item in container.ListBlobs(prefix: null, useFlatBlobListing: true, blobListingDetails: BlobListingDetails.None))
            {
                if (item.GetType() == typeof(CloudBlockBlob))
                {
                    CloudBlockBlob blockBlob = (CloudBlockBlob)item;
                    WriteInfoLine(string.Format("{0} ({1} KB)", blockBlob.Name, blockBlob.Properties.Length / 1024));
                    no++;
                }
                else
                {
                    throw new NotImplementedException(item.GetType().Name);
                }
            }
            WriteInfoLine(string.Format("Total: {0} files", no));
        }

        private static void Clean(CloudBlobContainer container, string sourcePath)
        {
            SystemPowerStatus sps = new SystemPowerStatus();
            GetSystemPowerStatus(sps);
            if (sps._ACLineStatus != ACLineStatus.Online)
                WriteWarning("Warning: The computer is not on AC power.");
            SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS | EXECUTION_STATE.ES_SYSTEM_REQUIRED);

            if (!sourcePath.EndsWith("\\"))
                sourcePath += "\\";
            int no = 0;
            // enumerate all the blobs
            foreach (IListBlobItem item in container.ListBlobs(prefix: null, useFlatBlobListing: true, blobListingDetails: BlobListingDetails.None))
            {
                if (item.GetType() == typeof(CloudBlockBlob))
                {
                    CloudBlockBlob blockBlob = (CloudBlockBlob)item;
                    if (!File.Exists(sourcePath + blockBlob.Name)) // File does not exist locally so delete it in the backup.
                    {
                        WriteInfo(string.Format("Backup of {0} ({1} KB): ", blockBlob.Name, blockBlob.Properties.Length / 1024));
                        blockBlob.Delete();
                        WriteInfoLineEnd("deleted.");
                        no++;
                    }
                }
                else
                {
                    throw new NotImplementedException(item.GetType().Name);
                }
            }
            WriteInfoLine(string.Format("{0} file(s) deleted from backup.", no));
            SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS);
        }

        private static void WriteError(string msg)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.Error.WriteLine(msg);
            Console.ResetColor();
            File.AppendAllText(logPath, string.Format("\r\n{0}\tError\t{1}", DateTime.Now, msg));
            noErrors++;
        }

        private static void WriteWarning(string msg)
        {
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.Out.WriteLine(msg);
            Console.ResetColor();
            File.AppendAllText(logPath, string.Format("\r\n{0}\tWarning\t{1}", DateTime.Now, msg));
            noWarnings++;
        }

        private static void WriteInfo(string msg)
        {
            Console.Out.Write(msg);
            File.AppendAllText(logPath, string.Format("\r\n{0}\tInfo\t{1}", DateTime.Now, msg));
        }

        private static void WriteInfoLineEnd(string msg)
        {
            Console.Out.WriteLine(msg);
            File.AppendAllText(logPath, msg);
        }

        private static void WriteInfoLine(string msg)
        {
            Console.Out.WriteLine(msg);
            File.AppendAllText(logPath, string.Format("\r\n{0}\tInfo\t{1}", DateTime.Now, msg));
        }

        private static void SendEmail(string name, BackupResult result)
        {
            if (Properties.Settings.Default.SendEmailNotification)
            {
                using (var smtpClient = new SmtpClient())
                {
                    smtpClient.EnableSsl = Properties.Settings.Default.EnableSSL;
                    WriteInfoLine(string.Format("Sending notification e-mail to {0} via {1}, port {2}...", Properties.Settings.Default.SendEmailNotificationAddress, smtpClient.Host, smtpClient.Port));
                    using (var mailMsg = new MailMessage(
                        Properties.Settings.Default.SendEmailNotificationAddress,
                        Properties.Settings.Default.SendEmailNotificationAddress))
                    {
                        mailMsg.Subject = "AzureBackup " + name;
                        if (noErrors > 0)
                            mailMsg.Subject += " failed";
                        else if (noWarnings > 0)
                            mailMsg.Subject += " partially succeeded";
                        else
                            mailMsg.Subject += " succeeded";
                        mailMsg.IsBodyHtml = false;
                        mailMsg.Body += "Number of source files: " + result.Files;
                        mailMsg.Body += "\r\nNumber of created files: " + result.Created;
                        mailMsg.Body += "\r\nNumber of updated files: " + result.Updated;
                        mailMsg.Body += "\r\nNumber of unchanged files: " + result.Unchanged;
                        mailMsg.Body += "\r\nNumber of errors: " + noErrors;
                        mailMsg.Body += "\r\nNumber of warnings: " + noWarnings;
                        mailMsg.Attachments.Add(new Attachment(logPath));
                        smtpClient.Send(mailMsg);
                    }
                    WriteInfoLine("E-mail sent.");
                }
            }
        }
    }
}
